# coding: utf-8

from typing import Optional, Sequence, Any, Tuple
from io import StringIO
from igraph import Graph as Digraph, OUT, IN
from utils import reverse_graph, try_get_value

vertex_t = int
edge_t = int
INVALID_VERTEX: vertex_t = -1
INVALID_EDGE: edge_t = -1

"""
  Basic clas for representing a directed graph.
"""


class Graph:
    g: Digraph

    def __init__(self, g: Digraph):
        self.g = g

    # TODO: 当前未处理多源多汇, 可能存在多个源点汇点
    def get_source(self):
        source = INVALID_VERTEX
        for v in self.g.get_all_nodes():
            if self.indeg[v] == 0:
                source = v
                break
        return source
    
    def get_target(self):
        target = INVALID_VERTEX
        for v in self.g.get_all_nodes():
            if self.outdeg[v] == 0:
                target = v
                break
        return target

    def get_node_attr(self, u: vertex_t, name: str):
        # print('get_node_attr:', u, name)
        return self.g.vs[u][name]

    def get_edge_attr(self, e: edge_t, name: str):
        return self.g.es[e][name]

    def get_uv_edge_attr(self, u: vertex_t, v: vertex_t, name: str, directed=True):
        e = self.get_edge(u, v, directed)
        if e == INVALID_EDGE:
            return None
        else:
            return self.g.es[e][name]

    def get_edge(self, u: vertex_t, v: vertex_t, directed=True) -> edge_t:
        e = self.g.get_eid(u, v, directed=directed, error=False)
        return e if e >= 0 else INVALID_EDGE

    def get_edge_uv_tuple(self, e: edge_t) -> Optional[Tuple[vertex_t, vertex_t]]:
        try:
            return self.g.es[e].tuple
        except:
            return None

    def set_node_attr(self, u: vertex_t, name: str, value: Any):
        try:
            self.g.vs[u][name] = try_get_value(value, u)
            return True
        except Exception as err:
            print(f"[set_node_attr]出现错误:{err}")
            return False

    def set_edge_attr(self, e: edge_t, name: str, value: Any) -> bool:
        try:
            self.g.es[e][name] = try_get_value(value, e)
            return True
        except Exception as err:
            print(f"[set_edge_attr]出现错误:{err}")
            return False

    def set_uv_edge_attr(self, u: vertex_t, v: vertex_t, name: str, value: Any):
        try:
            e = self.get_edge(u, v)
            if e != INVALID_EDGE:
                self.g.es[e][name] = try_get_value(value, e)
                return True
        except Exception as err:
            print(f"[set_uv_edge_attr]出现错误:{err}")
            return False

    def set_all_nodes_attr(self, name: str, value: Any, update_mode=False):
        for v in self.g.vs:
            if not update_mode or (update_mode and v[name] is None):
                v[name] = try_get_value(value, v.index)

    def set_all_edges_attr(self, name: str, value: Any, update_mode=False):
        for e in self.g.es:
            if not update_mode or (update_mode and e[name] is None):
                e[name] = try_get_value(value, e.index)

    def remove_all_nodes_attr(self, name: str):
        del self.g.vs[name]

    def remove_all_edges_attr(self, name: str):
        del self.g.es[name]

    """
     Add a new vertex to the graph.
     @return the identifier of the vertex
    """

    def add_node(self) -> vertex_t:
        v = self.g.add_vertex()
        # print('新增节点:', v.index)
        return v.index

    """'
     Add a new edge to the graph.
     The behavious is undefined if the same edge is added twice
     or if an identifier other then one returned by add_node() is used.
     @param from the identifier of the starting vertex
     @param to   the identifier of the ending vertex
     @return a reference to the graph for chaining multiple calls
    """

    def add_edge(self, u: vertex_t, v: vertex_t, auto_create_nodes=True):
        if auto_create_nodes:
            m = max(u, v)
            n = len(self.g.vs)
            if m > -1 and m >= n:
                for _ in range(m - n + 1):
                    self.add_node()
        _ = self.g.add_edge(u, v)
        # print(f'新增分支:{e.index}=({u}, {v})')
        return self

    """
      Get the number of vertices in the graph.
      @return the number of vertices
    """

    def node_count(self) -> int:
        return len(self.g.vs)

    def edge_count(self) -> int:
        return len(self.es)

    def has_node(self, u: vertex_t):
        return u > -1 and u < len(self.g.vs)

    """
      Get an immutable list of all successors.
    """

    def out_neighbours(self, u: vertex_t) -> Sequence[vertex_t]:
        return [self.g.es[i].target for i in self.g.incident(u, mode=OUT)]

    """
      Get an immutable list of all predecessors.
    """

    def in_neighbours(self, u: vertex_t) -> Sequence[vertex_t]:
        return [self.g.es[i].source for i in self.g.incident(u, mode=IN)]

    def out_neighbour(self, u: vertex_t, i: int) -> vertex_t:
        try:
            e = self.g.incident(u, mode=OUT)[i]
            return self.g.es[e].target
        except Exception:
            return INVALID_VERTEX

    def in_neighbour(self, u: vertex_t, i: int) -> vertex_t:
        try:
            e = self.g.incident(u, mode=IN)[i]
            return self.g.es[e].source
        except Exception:
            return INVALID_VERTEX

    # TODO  degree(type="out") 改为 outdegree()

    def out_degree(self, u: vertex_t) -> int:
        return self.g.vs[u].outdegree()

    def in_degree(self, u: vertex_t) -> int:
        return self.g.vs[u].indegree()

    """
      Get an implementation defined object which can be used for iterating through the vertices.
      The returned object can be used in a range for loop.
      It provides begin() and end() methods which yield forward iterators for iterating through the vertices.
    """

    def get_all_nodes(self) -> Sequence[vertex_t]:
        return [u.index for u in self.g.vs]

    def get_all_edges(self) -> Sequence[edge_t]:
        return [e.index for e in self.g.es]

    """
      Remove the given edge.
      Slow operation - should be avoided if possible.
    """

    def remove_edge(self, u: vertex_t, v: vertex_t):
        self.g.delete_edges([(u, v)])

    def reverse(self, copy_graph=True):
        g = reverse_graph(self.g, copy_graph=copy_graph)
        if copy_graph:
            return Graph(g)
        else:
            return self

    # TODO

    def delete_nodes(self, nodes):
        self.g.delete_vertices(nodes)

    def __str__(self):
        out = StringIO()
        for u in self.get_all_nodes():
            out.write(f"{u}:[")
            sep = ""
            for v in self.out_neighbours(u):
                out.write(f"{sep}{v}")
                sep = ", "
            out.write("]\n")
        return out.getvalue()
